package eval

import (
	"fmt"
	"showen/obj"
)

// Apply applies a procedure to a list of arguments
func Apply(args []obj.Object, env *obj.Environment) obj.Object {
	if len(args) < 2 {
		return &Error{Message: "apply requires at least 2 arguments"}
	}

	// First argument must be a procedure
	procedure := args[0]
	if procedure.Type() != obj.PROCEDURE_OBJ && procedure.Type() != obj.BUILTIN_OBJ && procedure.Type() != obj.LAMBDA_OBJ {
		return &Error{Message: "first argument to apply must be a procedure"}
	}

	// All arguments except the last one are additional arguments
	// The last argument must be a list
	if len(args) == 2 {
		// Only procedure and list
		list := args[1]
		if list.Type() != obj.PAIR_OBJ && list.Type() != obj.EMPTYLIST_OBJ {
			return &Error{Message: "last argument to apply must be a list"}
		}

		// Convert list to arguments
		var applyArgs []obj.Object
		if list.Type() == obj.PAIR_OBJ {
			applyArgs = listToSlice(list)
		}

		// Apply the procedure
		return applyProc(procedure, applyArgs, env)
	} else {
		// Additional arguments followed by list
		list := args[len(args)-1]
		if list.Type() != obj.PAIR_OBJ && list.Type() != obj.EMPTYLIST_OBJ {
			return &Error{Message: "last argument to apply must be a list"}
		}

		// Combine additional arguments with list elements
		var applyArgs []obj.Object
		// Add additional arguments (except procedure and list)
		for i := 1; i < len(args)-1; i++ {
			applyArgs = append(applyArgs, args[i])
		}
		// Add list elements
		if list.Type() == obj.PAIR_OBJ {
			applyArgs = append(applyArgs, listToSlice(list)...)
		}

		// Apply the procedure
		return applyProc(procedure, applyArgs, env)
	}
}

// Map applies a procedure to each element of one or more lists
func Map(args []obj.Object, env *obj.Environment) obj.Object {
	if len(args) < 2 {
		return &Error{Message: "map requires at least 2 arguments"}
	}

	// First argument must be a procedure
	procedure := args[0]
	if procedure.Type() != obj.PROCEDURE_OBJ && procedure.Type() != obj.BUILTIN_OBJ && procedure.Type() != obj.LAMBDA_OBJ {
		return &Error{Message: "first argument to map must be a procedure"}
	}

	// All remaining arguments must be lists
	lists := args[1:]
	for _, list := range lists {
		if list.Type() != obj.PAIR_OBJ && list.Type() != obj.EMPTYLIST_OBJ {
			return &Error{Message: "arguments to map must be lists"}
		}
	}

	// Check if all lists have the same length
	lengths := make([]int, len(lists))
	for i, list := range lists {
		lengths[i] = getListLength(list)
	}

	// If any list is empty, return empty list
	if lengths[0] == 0 {
		return obj.EMPTY_LIST
	}

	// Check if all lists have the same length
	for i := 1; i < len(lengths); i++ {
		if lengths[i] != lengths[0] {
			return &Error{Message: "all lists must have the same length"}
		}
	}

	// Apply procedure to each set of elements
	var results []obj.Object
	for i := 0; i < lengths[0]; i++ {
		// Get the i-th element from each list
		var procArgs []obj.Object
		for _, list := range lists {
			element := getListElement(list, i)
			if element == nil {
				return &Error{Message: "error accessing list element"}
			}
			procArgs = append(procArgs, element)
		}

		// Apply procedure to these elements
		result := applyProc(procedure, procArgs, env)
		if result.Type() == obj.ERROR_OBJ {
			return result
		}
		results = append(results, result)
	}

	// Convert results to a list
	return sliceToList(results)
}

// ForEach applies a procedure to each element of one or more lists for side effects
func ForEach(args []obj.Object, env *obj.Environment) obj.Object {
	if len(args) < 2 {
		return &Error{Message: "for-each requires at least 2 arguments"}
	}

	// First argument must be a procedure
	procedure := args[0]
	if procedure.Type() != obj.PROCEDURE_OBJ && procedure.Type() != obj.BUILTIN_OBJ && procedure.Type() != obj.LAMBDA_OBJ {
		return &Error{Message: "first argument to for-each must be a procedure"}
	}

	// All remaining arguments must be lists
	lists := args[1:]
	for _, list := range lists {
		if list.Type() != obj.PAIR_OBJ && list.Type() != obj.EMPTYLIST_OBJ {
			return &Error{Message: "arguments to for-each must be lists"}
		}
	}

	// Check if all lists have the same length
	lengths := make([]int, len(lists))
	for i, list := range lists {
		lengths[i] = getListLength(list)
	}

	// If any list is empty, return null object
	if lengths[0] == 0 {
		return obj.NULL_OBJECT
	}

	// Check if all lists have the same length
	for i := 1; i < len(lengths); i++ {
		if lengths[i] != lengths[0] {
			return &Error{Message: "all lists must have the same length"}
		}
	}

	// Apply procedure to each set of elements
	for i := 0; i < lengths[0]; i++ {
		// Get the i-th element from each list
		var procArgs []obj.Object
		for _, list := range lists {
			element := getListElement(list, i)
			if element == nil {
				return &Error{Message: "error accessing list element"}
			}
			procArgs = append(procArgs, element)
		}

		// Apply procedure to these elements
		result := applyProc(procedure, procArgs, env)
		if result.Type() == obj.ERROR_OBJ {
			return result
		}
	}

	// for-each returns nil (null object)
	return obj.NULL_OBJECT
}

// Helper functions

// listToSlice converts a list to a slice of objects
func listToSlice(list obj.Object) []obj.Object {
	var result []obj.Object
	current := list
	for current.Type() == obj.PAIR_OBJ {
		pair := current.(*obj.PairObject)
		if obj, ok := pair.Car.(obj.Object); ok {
			result = append(result, obj)
		}
		// Check if Cdr is a PairObject or EmptyList
		if pair.Cdr == nil || pair.Cdr == obj.EMPTY_LIST {
			break
		}
		if nextPair, ok := pair.Cdr.(*obj.PairObject); ok {
			current = nextPair
		} else if nextObj, ok := pair.Cdr.(obj.Object); ok && nextObj.Type() == obj.EMPTYLIST_OBJ {
			break
		} else {
			break
		}
	}
	return result
}

// sliceToList converts a slice of objects to a list
func sliceToList(slice []obj.Object) obj.Object {
	if len(slice) == 0 {
		return obj.EMPTY_LIST
	}

	// Build list from right to left
	var result any = obj.EMPTY_LIST
	for i := len(slice) - 1; i >= 0; i-- {
		result = &obj.PairObject{Car: slice[i], Cdr: result}
	}
	return result.(*obj.PairObject)
}

// getListLength returns the length of a list
func getListLength(list obj.Object) int {
	if list.Type() == obj.EMPTYLIST_OBJ {
		return 0
	}

	count := 0
	current := list
	for current.Type() == obj.PAIR_OBJ {
		pair := current.(*obj.PairObject)
		count++
		// Check if this is the last element
		if pair.Cdr == nil || pair.Cdr == obj.EMPTY_LIST {
			break
		}
		// Move to next element
		if nextPair, ok := pair.Cdr.(*obj.PairObject); ok {
			current = nextPair
		} else if nextObj, ok := pair.Cdr.(obj.Object); ok && nextObj.Type() == obj.EMPTYLIST_OBJ {
			break
		} else {
			break
		}
	}
	return count
}

// getListElement returns the element at the specified index in a list
func getListElement(list obj.Object, index int) obj.Object {
	if list.Type() == obj.EMPTYLIST_OBJ {
		return nil
	}

	current := list
	for i := 0; i < index && current.Type() == obj.PAIR_OBJ; i++ {
		pair := current.(*obj.PairObject)
		// Check if this is the last element
		if pair.Cdr == nil || pair.Cdr == obj.EMPTY_LIST {
			return nil
		}
		// Move to next element
		if nextPair, ok := pair.Cdr.(*obj.PairObject); ok {
			current = nextPair
		} else if nextObj, ok := pair.Cdr.(obj.Object); ok && nextObj.Type() == obj.EMPTYLIST_OBJ {
			return nil
		} else {
			return nil
		}
	}

	if current.Type() == obj.PAIR_OBJ {
		pair := current.(*obj.PairObject)
		if obj, ok := pair.Car.(obj.Object); ok {
			return obj
		}
	}

	return nil
}

// applyProc applies a procedure to arguments
func applyProc(procedure obj.Object, args []obj.Object, env *obj.Environment) obj.Object {
	switch proc := procedure.(type) {
	case *obj.Builtin:
		return proc.Fn(args, env)
	case *obj.Procedure:
		// Create new environment
		newEnv := obj.NewEnclosedEnvironment(proc.Env)
		// Bind parameters to arguments
		for i, param := range proc.Params {
			if i < len(args) {
				newEnv.Set(param, args[i])
			}
		}
		// Evaluate procedure body
		var result obj.Object
		var err error
		for _, expr := range proc.Body {
			result, err = EvalExpression(expr, newEnv)
			if err != nil {
				return &Error{Message: err.Error()}
			}
		}
		return result
	case *obj.Lambda:
		// Create new environment
		newEnv := obj.NewEnclosedEnvironment(proc.Env)
		// Bind parameters to arguments
		for i, param := range proc.Parameters {
			if i < len(args) {
				newEnv.Set(param, args[i])
			}
		}
		// Evaluate lambda body
		result, err := EvalExpression(proc.Body, newEnv)
		if err != nil {
			return &Error{Message: err.Error()}
		}
		return result
	default:
		return &Error{Message: fmt.Sprintf("unknown procedure type: %v", procedure.Type())}
	}
}
